feat: implement raise_dispute and the escrow freeze_for_dispute hook - #49
Conversation
Turns the dispute-resolution contract's entry point from a todo!() panic into a working flow, plus the campaign-escrow hook it depends on. dispute-resolution::raise_dispute now persists a Dispute record under a fresh DisputeId, publishes DisputeRaised, and freezes the contested payout in escrow so it can't be claimed mid-review. Design decisions settled in this PR (documented at the call site): - Who may raise: either side of the payout — the creator, or the campaign's business. Business ownership lives in campaign-escrow, so it's verified via a cross-contract get_campaign_business read rather than trusted from the caller. A stranger naming themselves creator is rejected because escrow refuses to freeze a campaign/creator pair with no settleable application. - Time window: none. The bound that protects funds is "before claim", which escrow already enforces by refusing to freeze an already-Paid application. This contract can't see proof-submission timestamps anyway. - Repeat disputes: one open dispute per (campaign_id, creator); a second attempt fails with DisputeAlreadyRaised. campaign-escrow::freeze_for_dispute is implemented as a per-application freeze (not a campaign-wide halt) so one contested creator doesn't stall payouts to everyone else on the same brief. It's authenticated to the configured dispute_contract, and the new `frozen` flag blocks claim_payment, submit_proof, approve_submission and reject_submission — the last three because the proof is the evidence the arbiter reviews. resolve_dispute clears the flag on settlement. The dispute contract declares a hand-written #[contractclient] for the two escrow methods it calls rather than depending on the escrow crate at build time, so escrow's contract exports don't leak into the dispute wasm; escrow is a dev-dependency only, for the cross-contract tests. resolve_dispute_payout remains todo!() — see its doc comment; it must clear `frozen` when it lands, as the admin resolve_dispute shortcut already does.
|
Merged, nice work — this is a solid piece of the arbitration flow, and the design write-up in the description made review straightforward. The per-application freeze (rather than a campaign-wide halt) and the require_auth-on-the-callee cross-contract auth pattern are both exactly right. Your branch was cut before #31 and #34 landed on main, which caused one real conflict: PayoutFrozen independently claimed error discriminant 24, the same slot #31 had already given to InvalidCreatorCount. Since I don't have write access to push to your fork branch, I resolved it directly — bumped PayoutFrozen to 25, no other change — and merged via a regular merge commit rather than GitHub's squash button, which is why the merge commit shows both of us as authors. Re-verified after resolving: fmt, full workspace build, all 97 tests (80 escrow + 17 dispute-resolution), clippy -D warnings, and the wasm32v1-none release build all clean. CI is green on main: https://github.com/Ads-Bazaar/ads-bazaar-contract/actions/runs/30161534915 Also confirmed #34's propose_admin/accept_admin (merged after your branch was cut) survived the merge intact alongside your changes — no other regressions. |
…50) storage::add_campaign_applicant rewrote an ever-growing Vec<Address> on every apply_to_campaign call just to answer a yes/no question in has_campaign_applicants. Replaces it with a u32 counter under DataKey::ApplicantCount, matching the existing approved_count pattern on Campaign, so applying costs O(1) storage-writes regardless of how many creators already applied. Adds a regression test that applies 200+ creators and asserts (via env.cost_estimate()) that the storage write cost of a later apply matches an early one, plus confirms update_campaign_metadata's lock-after-first-application behavior from #38 is unaffected. Closes #43 Verified locally in an isolated worktree before merge: - cargo fmt --all -- --check - cargo build --workspace - cargo test --workspace (64 tests) - cargo clippy --workspace --all-targets -- -D warnings - cargo build --workspace --target wasm32v1-none --release - Confirmed via git merge-tree that the merge combines cleanly with #34 and #49, which landed after this branch was cut.
What this does
Turns
dispute-resolution::raise_disputefrom atodo!()panic into a working flow, and implements thecampaign-escrow::freeze_for_disputehook it depends on.raise_disputeis the entry point for the whole arbitration flow — nothing else in the contract could be exercised until aDisputerecord actually got created.raise_disputenow persists aDisputeunder a freshDisputeId, publishesDisputeRaised, and freezes the contested payout in escrow via a real cross-contract call so it can't be claimed mid-review.Design decisions settled here
Both are documented at the call site.
creator, or the campaign'sbusiness. Business ownership lives in campaign-escrow, so it's verified via a cross-contractget_campaign_businessread rather than trusted from the caller. A stranger naming themselves creator is rejected because escrow refuses to freeze a(campaign_id, creator)pair with no settleable application.Paidapplication. This contract can't see proof-submission timestamps anyway, so a day-based deadline would only add a second, weaker rule.(campaign_id, creator); a second attempt fails withDisputeAlreadyRaised.Escrow side
freeze_for_disputeis a per-application freeze (not a campaign-wide halt) so one contested creator doesn't stall payouts to everyone else on the same brief. It's authenticated to the configureddispute_contract. A newfrozenflag onApplicationblocksclaim_payment,submit_proof,approve_submissionandreject_submission— the last three because the proof is the evidence the arbiter reviews. The existing adminresolve_disputeclears the flag on settlement.To keep escrow's contract exports out of the dispute-resolution wasm, the dispute contract declares a hand-written
#[contractclient]for the two escrow methods it calls rather than depending on the escrow crate at build time; escrow is a dev-dependency only, for the cross-contract tests.Not in scope
resolve_dispute_payoutremainstodo!()— its doc comment now notes it must clearfrozenwhen it lands, the way the adminresolve_disputeshortcut already does.Note for reviewers
This adds a
frozen: boolfield to theApplicationstruct, changing its stored layout. Fine on a fresh deploy; a live network with existing escrow state would need a migration. Assumed fresh deploy given the repo is a pre-launch scaffold.Testing
Mirrors
.github/workflows/ci.yml— all pass locally:cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo test --workspace— 67 + 17 pass (was 67 + 7)cargo build --workspace --target wasm32v1-none --release— exit 0The old
raise_dispute_is_not_yet_implemented#[should_panic]test is replaced by an 11-testtest_raise_disputemodule (incrementing IDs, stored-field correctness, business-raised disputes, unauthorized-caller rejection, escrow freeze, duplicate rejection, post-deadline raising), plus 11 new escrow-sidefreeze_for_disputetests.closes #40